﻿import L from "leaflet"
import currentMarkPng from './current_mark.png'

/**转换流场UV值*/
let UV2SDCur = function (u, v) {
    let spd, dir
    if (u === 0 && v === 0) {
        spd = 0
        dir = -9999
        return { speed: spd, dir: dir }
    }

    if (u === 0 || v === 0) {
        if (u === 0) {
            spd = v < 0 ? -v : v
            if (v > 0) { dir = Math.PI / 2 }
            else if (v < 0) { dir = Math.PI * 3 / 2 }
        } else if (v === 0) {
            spd = u < 0 ? -u : u
            if (u > 0) { dir = 0 }
            else if (u < 0) { dir = Math.PI }
        }
        dir = (2.5 * Math.PI - dir) % (Math.PI * 2)
        return { speed: spd, dir: dir }
    }

    spd = Math.sqrt(u * u + v * v)
    dir = Math.atan(u / v)
    if (u < 0 && v > 0) { dir = 1 / 2.0 * Math.PI - dir }
    else if (u < 0 && v < 0) { dir = 3 / 2.0 * Math.PI - dir }
    else if (u > 0 && v < 0) { dir = 3 / 2.0 * Math.PI - dir }
    else if (u > 0 && v > 0) { dir = 1 / 2.0 * Math.PI - dir }
    dir = (2.5 * Math.PI - dir) % (Math.PI * 2)
    return { speed: spd, dir: dir }
}

if (!L.DomUtil.setTransform) {
    L.DomUtil.setTransform = function (el, offset, scale) {
        let pos = offset || new L.Point(0, 0);
        el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '');
    };
}

L.CanvasLayer = (L.Layer ? L.Layer : L.Class).extend({

    initialize: function initialize(options) {
        this._map = null;
        this._canvas = null;
        this._canvas_marker = null; //绘制流场箭头的图层
        this._range = null;
        this._frame = null;
        this._delegate = null;
        this.zIndex = null;
        L.setOptions(this, options);
    },

    delegate: function delegate(del) {
        this._delegate = del;
        return this;
    },

    needRedraw: function needRedraw() {
        if (!this._frame) {
            this._frame = L.Util.requestAnimFrame(this.drawLayer, this);
        }
        return this;
    },

    _onLayerDidResize: function _onLayerDidResize(resizeEvent) {
        this._canvas.width = resizeEvent.newSize.x * this.options.magnification;
        this._canvas.height = resizeEvent.newSize.y * this.options.magnification;
        this._canvas.style.width = resizeEvent.newSize.x + "px";
        this._canvas.style.height = resizeEvent.newSize.y + "px";
        this._canvas_marker.width = resizeEvent.newSize.x * this.options.magnification;
        this._canvas_marker.height = resizeEvent.newSize.y * this.options.magnification;
        this._canvas_marker.style.width = resizeEvent.newSize.x + "px";
        this._canvas_marker.style.height = resizeEvent.newSize.y + "px";
    },

    _onLayerDidMove: function _onLayerDidMove() {
        let topLeft = this._map.containerPointToLayerPoint([0, 0]);
        L.DomUtil.setPosition(this._canvas, topLeft);
        L.DomUtil.setPosition(this._canvas_marker, topLeft);
        this.drawLayer();
    },

    getEvents: function getEvents() {
        let events = {
            resize: this._onLayerDidResize,
            moveend: this._onLayerDidMove
        };
        if (this._map.options.zoomAnimation && L.Browser.any3d) {
            events.zoomanim = this._animateZoom;
        }
        return events;
    },

    onAdd: function onAdd(map) {
        this._map = map;
        this._canvas = L.DomUtil.create('canvas', 'leaflet-layer');
        this._canvas_marker = L.DomUtil.create('canvas', 'leaflet-layer');
        this.tiles = {};
        this._canvas.style.zIndex = this.options.zIndex;
        this._canvas.style.pointerEvents = "none";
        this._canvas_marker.style.zIndex = this.options.zIndex - 1;
        this._canvas_marker.style.pointerEvents = "none";
        let size = this._map.getSize();
        this._canvas.width = size.x * this.options.magnification;
        this._canvas.height = size.y * this.options.magnification;
        this._canvas.style.width = size.x + "px";
        this._canvas.style.height = size.y + "px";
        this._canvas_marker.width = size.x * this.options.magnification;
        this._canvas_marker.height = size.y * this.options.magnification;
        this._canvas_marker.style.width = size.x + "px";
        this._canvas_marker.style.height = size.y + "px";

        let animated = this._map.options.zoomAnimation && L.Browser.any3d;
        L.DomUtil.addClass(this._canvas, 'leaflet-zoom-' + (animated ? 'animated' : 'hide'));
        L.DomUtil.addClass(this._canvas_marker, 'leaflet-zoom-' + (animated ? 'animated' : 'hide'));

        map._panes.overlayPane.appendChild(this._canvas);
        map._panes.overlayPane.appendChild(this._canvas_marker);
        map.on(this.getEvents(), this);

        let del = this._delegate || this;
        del.onLayerDidMount && del.onLayerDidMount();
        this.needRedraw();

        let self = this;
        setTimeout(function () {
            self._onLayerDidMove();
        }, 0);
    },

    onRemove: function onRemove(map) {
        let del = this._delegate || this;
        del.onLayerWillUnmount && del.onLayerWillUnmount();
        map.getPanes().overlayPane.removeChild(this._canvas);
        map.getPanes().overlayPane.removeChild(this._canvas_marker);
        map.off(this.getEvents(), this);
        this._canvas = null;
        this._canvas_marker = null;
    },

    addTo: function addTo(map) {
        map.addLayer(this);
        return this;
    },

    LatLonToMercator: function LatLonToMercator(latlon) {
        return {
            x: latlon.lng * 6378137 * Math.PI / 180,
            y: Math.log(Math.tan((90 + latlon.lat) * Math.PI / 360)) * 6378137
        };
    },

    drawLayer: function drawLayer() {
        let size = this._map.getSize();
        let bounds = this._map.getBounds();
        let zoom = this._map.getZoom();
        let center = this.LatLonToMercator(this._map.getCenter());
        let corner = this.LatLonToMercator(this._map.containerPointToLatLng(this._map.getSize()));
        let del = this._delegate || this;
        del.onDrawLayer && del.onDrawLayer({
            layer: this,
            canvas: this._canvas,
            canvas_marker: this._canvas_marker,
            bounds: bounds,
            size: size,
            zoom: zoom,
            center: center,
            corner: corner
        });
        this._frame = null;
    },

    _setTransform: function _setTransform(el, offset, scale) {
        let pos = offset || new L.Point(0, 0);
        el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '');
    },

    _animateZoom: function _animateZoom(e) {
        let scale = this._map.getZoomScale(e.zoom);
        let offset = L.Layer ? this._map._latLngToNewLayerPoint(this._map.getBounds().getNorthWest(), e.zoom, e.center) : this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos());
        L.DomUtil.setTransform(this._canvas, offset, scale);
        L.DomUtil.setTransform(this._canvas_marker, offset, scale);
    }
});

L.canvasLayer = function (options) {
    return new L.CanvasLayer(options);
};

L.Control.Velocity = L.Control.extend({

    options: {
        position: 'bottomleft',
        emptyString: 'Unavailable',
        angleConvention: 'bearingCCW',
        speedUnit: 'm/s'
    },

    onAdd: function onAdd() {
        this._container = L.DomUtil.create('div', 'leaflet-control-velocity');
        L.DomEvent.disableClickPropagation(this._container);
        return this._container;
    },

    onRemove: function onRemove() { },

    vectorToSpeed: function vectorToSpeed(uMs, vMs, unit) {
        let velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
        if (unit === 'k/h') {
            return this.meterSec2kilometerHour(velocityAbs);
        } else if (unit === 'kt') {
            return this.meterSec2Knots(velocityAbs);
        } else {
            return velocityAbs;
        }
    },

    vectorToDegrees: function vectorToDegrees(uMs, vMs, angleConvention) {
        if (angleConvention.endsWith('CCW')) {
            vMs = vMs > 0 ? vMs = -vMs : Math.abs(vMs);
        }
        let velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
        let velocityDir = Math.atan2(uMs / velocityAbs, vMs / velocityAbs);
        let velocityDirToDegrees = velocityDir * 180 / Math.PI + 180;
        if (angleConvention === 'bearingCW' || angleConvention === 'meteoCCW') {
            velocityDirToDegrees += 180;
            if (velocityDirToDegrees >= 360) velocityDirToDegrees -= 360;
        }
        return velocityDirToDegrees;
    },

    meterSec2Knots: function meterSec2Knots(meters) {
        return meters / 0.514;
    },

    meterSec2kilometerHour: function meterSec2kilometerHour(meters) {
        return meters * 3.6;
    }
});

L.Map.mergeOptions({
    positionControl: false
});

L.Map.addInitHook(function () {
    if (this.options.positionControl) {
        this.positionControl = new L.Control.MousePosition();
        this.addControl(this.positionControl);
    }
});

L.control.velocity = function (options) {
    return new L.Control.Velocity(options);
};

L.ParticleLayer = (L.Layer ? L.Layer : L.Class).extend({

    options: {
        displayValues: true,
        displayOptions: { velocityType: '', displayPosition: '', displayEmptyString: '' },
        maxVelocity: 10,
        data: null,
        zIndex: 0,
        magnification: 1, //画布缩放比例
        uvExaggerate: 1,//uv夸张系数
        color: ["rgba(250,250,250,0.9)"],
        velocityScale: 0.005, //步长，流动速度(尽量不变)
        globalAlpha: 0.90, //全局透明度
        particleAge: 30, //粒子生命周期
        particleMultiplier: 350, //密度
        frame_rate: 30, //刷新率
        lineWidth: 1, //线宽
        type: "wind", //wind wavedir current
        particle: true,
        arrow: true,
        callback: null,
        uData: null,
        vData: null,
        calRegion: false,
        range: {
            scale: 0,
            startLat: 0,
            startLon: 0,
            endLon: 0,
            endLat: 0,
            width: 0,
            height: 0,
        },
    },

    _map: null,
    _canvasLayer: null,
    _windy: null,
    _context: null,
    _timer: 0,
    _mouseControl: null,

    initialize: function initialize(options) {
        L.setOptions(this, options);
    },

    onAdd: function onAdd(map) {
        this._canvasLayer = L.canvasLayer(this.options).delegate(this);
        this._canvasLayer.addTo(map);
        this._map = map;
    },

    onRemove: function onRemove() {
        this._destroyWind();
    },

    setData: function setData(data) {
        this.options.data = data;
        try {
            if (this._windy) {
                this._windy.setData(data);
                this._clearAndRestart();
            }
            this.fire('load');
        } catch (e) {
            console.log(e);
        }
    },

    imgCanvas: document.createElement("canvas"), //提前创建存放图像数据的canvas
    imgData: null, //读取到内存中的图像数据
    _setdata: function _setdata() {
        let self = this
        let uHeader = {
            "lo1": self.options.range.startLon,
            "la1": self.options.range.startLat,
            "d": self.options.range.scale,
            "nx": self.options.range.width,
            "ny": self.options.range.height,
            "nodata": -999,
            "dataType": "windU"
        }
        let vHeader = {
            "lo1": self.options.range.startLon,
            "la1": self.options.range.startLat,
            "d": self.options.range.scale,
            "nx": self.options.range.width,
            "ny": self.options.range.height,
            "nodata": -999,
            "dataType": "windV"
        }
        let ParticleData = [
            { header: uHeader, data: self.options.uData },
            { header: vHeader, data: self.options.vData }
        ]
        self.options.data = ParticleData
        if (self._windy) {
            self._windy.setData(ParticleData)
            self._clearAndRestart()
        }
        self.fire('load')
    },

    /**绘制流场【箭头】 */
    drawArrow: function drawArrow() {
        let self = this;
        let NewData = self.options.data
        let lo1 = self.options.range.startLon
        let la1 = self.options.range.startLat
        let d = self.options.range.scale
        let nx = parseInt((self.options.range.endLon - self.options.range.startLon) / d) + 1
        let ny = parseInt((self.options.range.startLat - self.options.range.endLat) / d) + 1
        //NewData[0].data是U值+NewData[1].data是V值

        let _canvas_marker = this._canvasLayer._canvas_marker;
        /**绘图环境 */
        let ctx_marker = _canvas_marker.getContext("2d");
        let Cwidth = _canvas_marker.width;
        let Cheight = _canvas_marker.height;
        ctx_marker.clearRect(0, 0, Cwidth, Cheight); //清空上一次绘制

        let datadata = self._calMaxMin(NewData[0].data, NewData[1].data);
        let dataArray = datadata[2]; //存储计算后的速度、方向
        let dataMax = datadata[0];
        let dataMin = datadata[1];
        let dataMiddle = dataMin + (dataMax - dataMin) / 4;
        let labelcell = 36; //间隔暂定为64像素
        let labelcol = Math.ceil(Cwidth / labelcell);
        let labelrow = Math.ceil(Cheight / labelcell);
        for (let i = 0; i < labelrow; i++) {
            for (let j = 0; j < labelcol; j++) {
                try {
                    let x = j * labelcell;
                    let y = i * labelcell;
                    let point0 = self._map.containerPointToLatLng(L.point(x, y));
                    let lat0 = point0.lat;
                    let lon0 = point0.lng;
                    lon0 = lon0 % 360;
                    if (lon0 < 0) {
                        lon0 = lon0 + 360;
                    }
                    let _row = Math.floor((la1 - lat0) / d); //d是数据分辨率
                    let _col = Math.floor((lon0 - lo1) / d); //d是数据分辨率
                    if (_row < 0 || _row >= ny || _col < 0 || _col >= nx) {
                        continue;
                    }
                    let index = _row * nx + _col;
                    let spdDir = dataArray[index];
                    if (spdDir.dir == -9999) { //无效值
                        continue;
                    } else {
                        let color = self._setcolor(spdDir.speed, dataMax, dataMin);
                        // let color = { r: 255, g: 220, b: 0, a: 1, }
                        let scalar = 1;//给流速较大的箭头加长
                        if (spdDir.speed > dataMiddle) { //大于中等流速的按比例加长0.75=3/4
                            scalar = scalar + (spdDir.speed - dataMiddle) / ((dataMax - dataMin)) * 0.5;
                        }
                        self._drawIcon(ctx_marker, x, y, spdDir.dir, color, scalar);
                    }
                } catch (e) {
                    console.log(e);
                    continue;
                }
            }
        }
    },

    /**计算当前流速的最大最小值 */
    _calMaxMin: function _calMaxMin(dataU, dataV) {
        let dataArray = [];
        let dataMax = 0;
        let dataMin = 0;
        for (let index = 0; index < dataU.length; index++) {
            let spdDir = UV2SDCur(dataU[index], dataV[index]);
            dataArray.push(spdDir);
            if (spdDir.speed > dataMax) {
                dataMax = spdDir.speed;
            } else if (spdDir.speed < dataMin) {
                dataMin = spdDir.speed;
            }
        }
        return [dataMax, dataMin, dataArray];
    },

    /**线性变化色带 */
    _setcolor: function _setcolor(speed, max, min) {
        let part = (max - min) / 16;
        let colorRamp = 256.0 / 16;
        if (speed >= max) {
            return {
                r: 255,
                g: 0,
                b: 0
            }
        } else if (speed <= min) {
            return {
                r: 255,
                g: 220,
                b: 0
            }
        } else {
            let cel = Math.round((speed - min) / part);
            let val = Math.round(cel * colorRamp);
            val = val > 220 ? 220 : val;
            return {
                r: 255,
                g: 220 - val,
                b: 0
            }
        }
    },

    /**canvas绘制流场箭头*/
    _drawIcon: function (context, x, y, angle, color, scalar) {
        scalar = scalar > 2 ? 2 : scalar
        let scale = 0.35;
        let image = new Image();
        image.src = currentMarkPng
        image.onload = function () {
            (function (scalar) {
                context.save();
                let w = image.width * scale;
                let h = image.height * scale * scalar;
                context.translate(x + w / 2, y + h / 2);
                context.rotate(angle); //弧度
                context.drawImage(image, -(w / 2), -(h / 2), w, h);
                let li = Math.sqrt(w * w + h * h); //斜边
                context.restore();
                let dataL = context.getImageData(x + w / 2 - li / 2, y + h / 2 - li / 2, li, li);
                for (let i = 0; i < dataL.data.length; i += 4) { //逐像素修改颜色透明度
                    if (dataL.data[i] === 0 && dataL.data[i + 1] === 0 && dataL.data[i + 2] === 0 && dataL.data[i + 3] === 0) {
                        continue;
                    }
                    dataL.data[i] = color.r;
                    dataL.data[i + 1] = color.g;
                    dataL.data[i + 2] = color.b;
                    if (color.a) {
                        dataL.data[i + 3] = dataL.data[i + 3] * color.a;
                    }
                }
                context.putImageData(dataL, x + w / 2 - li / 2, y + h / 2 - li / 2);
            })(scalar);
        };
    },

    /*------------------------------------ PRIVATE ------------------------------------------*/

    onDrawLayer: function onDrawLayer() {
        let self = this;
        if (!this._windy) {
            this._initWindy();
            if (self.options.data) {
                self._windy.setData(self.options.data);
            }
            return;
        }
        if (!this.options.data) {
            return;
        }
        if (this._timer) clearTimeout(self._timer);
        if (self.options.data.length === 0) {
            return;
        }
        self.CalcurrentRegion();
        self._clearAndRestart();
    },

    /**计算当前区域占比 */
    CalcurrentRegion: function CalcurrentRegion() {
        let self = this
        if (!self.options.range.calRegion) {return}
        let bound = this._map.getBounds();
        let LonSpan = this._map.distance([bound._southWest.lat, bound._southWest.lng], [bound._southWest.lat, bound._northEast.lng]);
        let LatSpan = this._map.distance([bound._northEast.lat, bound._southWest.lng], [bound._southWest.lat, bound._southWest.lng]);
        let MapArea = LonSpan * LatSpan;
        let startLon = self.options.range.startLon;
        let startLat = self.options.range.startLat;
        let endLon = self.options.range.endLon;
        let endLat = self.options.range.endLat;
        let curLonSpan = this._map.distance([startLat, startLon], [startLat, endLon]);
        let curLatSpan = this._map.distance([startLat, startLon], [endLat, startLon]);
        let curMapArea = curLonSpan * curLatSpan;
        let bili = (curMapArea / MapArea) * 3;
        bili = bili > 1 ? 1 : bili;
        if (this._windy) this._windy.setAreaOf(bili);
        // if (this._windy) this._windy.setAreaOf(1);

        //计算尺寸放大后的 粒子数量比例
        if (this._windy) this._windy.setMagnification(this.options.magnification);
    },

    _startWindy: function _startWindy() {
        let bounds = this._map.getBounds();
        let size = this._map.getSize();
        // bounds, width, height, extent
        let self = this;
        this._windy.start(
            [[0, 0], [size.x * self.options.magnification, size.y * self.options.magnification]],
            size.x * self.options.magnification,
            size.y * self.options.magnification,
            [[bounds._southWest.lng, bounds._southWest.lat], [bounds._northEast.lng, bounds._northEast.lat]]
        );
    },

    _initWindy: function _initWindy() {
        let self = this;
        // windy object, copy options
        let options = Object.assign({
            canvas: self._canvasLayer._canvas
        }, self.options);
        this._windy = new Windy(options);
        // prepare context global let, start drawing
        this._context = this._canvasLayer._canvas.getContext('2d');
        this._canvasLayer._canvas.classList.add("velocity-overlay");
        this.onDrawLayer();
        this._map.on('dragend', self._windy.clearCanvas);
        this._map.on('zoomstart', self._windy.clearCanvas);
        this._map.on('resize', self._clearWind);
        this._initMouseHandler();
    },

    _initMouseHandler: function _initMouseHandler() {
        if (!this._mouseControl && this.options.displayValues) {
            let options = this.options.displayOptions || {};
            options['leafletVelocity'] = this;
            this._mouseControl = L.control.velocity(options).addTo(this._map);
        }
    },

    _clearAndRestart: function _clearAndRestart() {
        this.CalcurrentRegion();
        if (this._context) this._context.clearRect(0, 0, 3000, 3000);
        if (this.options.particle) {
            if (this._windy) this._startWindy();
        } else {
            if (this._windy) this._windy.stop();
            if (this._context) this._context.clearRect(0, 0, 3000, 3000);
        }
        if (this.options.arrow) {
            this.drawArrow()
        } else {
            this._clearArrow()
        }
    },

    /**更改参数并重绘 */
    changeParamesReset: function changeParamesReset(params) {
        let self = this;
        if (params.color) {
            self.options.color = params.color;
            if (self._windy) {
                self._windy.resetColor(params.color);
            }
        }

        if (params.velocityScale) {
            self.options.velocityScale = params.velocityScale;
            if (self._windy) {
                self._windy.resetStepM(params.velocityScale);
            }
        }

        if (params.globalAlpha) {
            self.options.globalAlpha = params.globalAlpha;
            if (self._windy) {
                self._windy.resetGlobalAlpha(params.globalAlpha);
            }
        }

        if (params.particleAge) {
            self.options.particleAge = params.particleAge;
            if (self._windy) {
                self._windy.resetParticleAge(params.particleAge);
            }
        }

        if (params.particleMultiplier) {
            self.options.particleMultiplier = params.particleMultiplier;
            if (self._windy) {
                self._windy.resetNum(params.particleMultiplier);
            }
        }
        if (params.frame_rate) {
            self.options.frame_rate = params.frame_rate;
            if (self._windy) {
                self._windy.resetFrame(params.frame_rate);
            }
        }
        if (params.lineWidth) {
            self.options.lineWidth = params.lineWidth;
            if (self._windy) {
                self._windy.resetWidth(params.lineWidth);
            }
        }


        if (typeof params.arrow === 'boolean') {
            self.options.arrow = params.arrow
        }
        if (typeof params.particle === 'boolean') {
            self.options.particle = params.particle
        }

        if (self._windy) {
            self._clearAndRestart();
        }
    },

    _clearArrow: function _clearArrow() {
        if (!this._canvasLayer) {
            return
        }
        let _canvas_marker = this._canvasLayer._canvas_marker;
        let ctx_marker = _canvas_marker?.getContext("2d");
        ctx_marker?.clearRect(0, 0, _canvas_marker.width, _canvas_marker.height); //清空上一次绘制
    },

    _clearWind: function _clearWind() {
        if (this._windy) this._windy.stop();
        if (this._context) this._context.clearRect(0, 0, 3000, 3000);
        if (this._clearArrow) {
            this._clearArrow()
        }
    },

    _destroyWind: function _destroyWind() {
        if (this._timer) clearTimeout(this._timer);
        if (this._windy) this._windy.stop();
        if (this._context) this._context.clearRect(0, 0, 3000, 3000);
        if (this._mouseControl) this._map.removeControl(this._mouseControl);
        this._mouseControl = null;
        this._windy = null;
        this._map.removeLayer(this._canvasLayer);
        this._clearArrow()
    },

    _changetimeredraw: function () {
        if (this._timer) clearTimeout(this._timer);
        if (this._windy) this._windy.stop();
        if (this._context) this._context.clearRect(0, 0, 3000, 3000);
        if (this._mouseControl) this._map.removeControl(this._mouseControl);
        this._mouseControl = null;
        this._setdata();
    }
});

var Windy = function Windy(params) {
    var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; // velocity at which particle intensity is minimum (m/s)
    var MAX_VELOCITY_INTENSITY = params.maxVelocity || 40; // velocity at which particle intensity is maximum (m/s)
    var VELOCITY_SCALE = (params.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice)
    var MAX_PARTICLE_AGE = params.particleAge || 90; // max number of frames a particle is drawn before regeneration
    var PARTICLE_LINE_WIDTH = params.lineWidth || 2; // line width of a drawn particle
    var PARTICLE_MULTIPLIER = 1 / params.particleMultiplier || 1 / 300; // particle count scalar (completely arbitrary--this values looks nice)
    // var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount
    var FRAME_RATE = params.frame_rate || 20; // desired frames per second
    var GLOBALALPHA = params.globalAlpha || 0.9;
    var AREA_OF = 1; //区域占比(针对区域显示)
    var Magnification = 1; //绘制比例

    // var defaulColorScale = ["rgb(36,104, 180)", "rgb(60,157, 194)", "rgb(128,205,193)", "rgb(151,218,168)", "rgb(198,231,181)", "rgb(238,247,217)", "rgb(255,238,159)", "rgb(252,217,125)", "rgb(255,182,100)", "rgb(252,150,75)", "rgb(250,112,52)", "rgb(245,64,32)", "rgb(237,45,28)", "rgb(220,24,32)", "rgb(180,0,35)"];
    var colorScale = params.color;

    var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude]

    var builder;
    var grid;
    var gridData = params.data;
    var date;
    var λ0, φ0, Δλ, Δφ, ni, nj;

    var setData = function setData(data) {
        gridData = data;
    };

    // interpolation for vectors like wind (u,v,m)
    var bilinearInterpolateVector = function bilinearInterpolateVector(x, y, g00, g10, g01, g11) {
        var rx = 1 - x;
        var ry = 1 - y;
        var a = rx * ry,
            b = x * ry,
            c = rx * y,
            d = x * y;
        var u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d;
        var v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d;
        return [u, v, Math.sqrt(u * u + v * v)];
    };

    var createWindBuilder = function createWindBuilder(uComp, vComp) {
        var uData = uComp.data,
            vData = vComp.data;
        return {
            header: uComp.header,
            //recipe: recipeFor("wind-" + uComp.header.surface1Value),
            data: function data(i) {
                return [uData[i], vData[i]];
            },
            interpolate: bilinearInterpolateVector
        };
    };

    var createBuilder = function createBuilder(data) {
        var uComp = null,
            vComp = null;
        // var temp = null,
        //     scalar = null;
        data.forEach(function (record) {
            switch (record.header.dataType) {
                case "windU":
                    uComp = record;
                    break;
                case "windV":
                    vComp = record;
                    break;
                default:
                    vComp = record;
            }
        });
        return createWindBuilder(uComp, vComp);
    };

    var buildGrid = function buildGrid(data, callback) {
        builder = createBuilder(data);
        var header = builder.header;
        λ0 = header.lo1;
        φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N)
        Δλ = header.d;
        Δφ = header.d; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)
        ni = header.nx;
        nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73)

        date = new Date(header.refTime);
        date.setHours(date.getHours() + header.forecastTime);

        grid = [];
        var p = 0;
        var isContinuous = Math.floor(ni * Δλ) >= 360;
        for (var j = 0; j < nj; j++) {
            var row = [];
            for (var i = 0; i < ni; i++, p++) {
                row[i] = builder.data(p);
            }
            if (isContinuous) {
                // For wrapped grids, duplicate first column as last column to simplify interpolation logic
                row.push(row[0]);
            }
            grid[j] = row;
        }
        callback({
            date: date,
            interpolate: interpolate
        });
    };

    /**
     * Get interpolated grid value from Lon/Lat position
     * @param λ {Float} Longitude
     * @param φ {Float} Latitude
     * @returns {Object}
     */
    var interpolate = function interpolate(λ, φ) {
        if (!grid) return null;
        var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360)
        var j = (φ0 - φ) / Δφ; // calculate latitude index in direction +90 to -90
        var fi = Math.floor(i),
            ci = fi + 1;
        var fj = Math.floor(j),
            cj = fj + 1;
        var row = grid[fj];
        if (grid[fj]) {
            var g00 = row[fi];
            var g10 = row[ci];
            if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
                var g01 = row[fi];
                var g11 = row[ci];
                if (isValue(g01) && isValue(g11)) {
                    // All four points found, so interpolate the value.
                    return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11);
                }
            }
        }
        return null;
    };

    /**
     * @returns {Boolean} true if the specified value is not null and not undefined.
     */
    var isValue = function isValue(x) {
        return x !== null && x !== undefined;
    };

    /**
     * @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
     *          of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
     */
    var floorMod = function floorMod(a, n) {
        return a - n * Math.floor(a / n);
    };

    /**
     * Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind
     * vector is modified in place and returned by this function.
     */
    var distort = function distort(projection, λ, φ, x, y, scale, wind, windy) {
        var u = wind[0] * scale;
        var v = wind[1] * scale;
        var d = distortion(projection, λ, φ, x, y, windy);
        // Scale distortion vectors by u and v, then add.
        wind[0] = d[0] * u + d[2] * v;
        wind[1] = d[1] * u + d[3] * v;
        return wind;
    };

    var distortion = function distortion(projection, λ, φ, x, y, windy) {
        var τ = 2 * Math.PI;
        // var H = Math.pow(10, -5.2);
        var H = Math.pow(10, -6);
        var hλ = λ < 0 ? H : -H;
        var hφ = φ < 0 ? H : -H;
        var pλ = project(φ, λ + hλ, windy);
        var pφ = project(φ + hφ, λ, windy);
        // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ
        // changes depending on φ. Without this, there is a pinching effect at the poles.
        var k = Math.cos(φ / 360 * τ);
        return [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ];
    };

    var createField = function createField(columns, bounds, callback) {
        /**
         * @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind
         *          is undefined at that point.
         */
        function field(x, y) {
            var column = columns[Math.round(x)];
            return column && column[Math.round(y)] || NULL_WIND_VECTOR;
        }
        // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new
        // field is interpolated because the field closure's context is leaked, for reasons that defy explanation.
        field.release = function () {
            columns = [];
        };
        field.randomize = function (o) {
            // UNDONE: this method is terrible
            var x, y;
            var safetyNet = 0;
            do {
                x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x);
                y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y);
            } while (field(x, y)[2] === null && safetyNet++ < 30);
            o.x = x;
            o.y = y;
            return o;
        };
        callback(bounds, field);
    };

    var buildBounds = function buildBounds(bounds, width, height) {
        var upperLeft = bounds[0];
        var lowerRight = bounds[1];
        var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0);
        var y = Math.max(Math.floor(upperLeft[1], 0), 0);
        // var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1);
        var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1);
        return {
            x: x,
            y: y,
            xMax: width,
            yMax: yMax,
            width: width,
            height: height
        };
    };

    var deg2rad = function deg2rad(deg) {
        return deg / 180 * Math.PI;
    };

    var rad2deg = function rad2deg(ang) {
        return ang / (Math.PI / 180.0);
    };

    var invert = function invert(x, y, windy) {
        var mapLonDelta = windy.east - windy.west;
        var worldMapRadius = windy.width / rad2deg(mapLonDelta) * 360 / (2 * Math.PI);
        var mapOffsetY = worldMapRadius / 2 * Math.log((1 + Math.sin(windy.south)) / (1 - Math.sin(windy.south)));
        var equatorY = windy.height + mapOffsetY;
        var a = (equatorY - y) / worldMapRadius;

        var lat = 180 / Math.PI * (2 * Math.atan(Math.exp(a)) - Math.PI / 2);
        var lon = rad2deg(windy.west) + x / windy.width * rad2deg(mapLonDelta);
        return [lon, lat];
    };

    var mercY = function mercY(lat) {
        return Math.log(Math.tan(lat / 2 + Math.PI / 4));
    };

    var project = function project(lat, lon, windy) {
        // both in radians, use deg2rad if neccessary
        var ymin = mercY(windy.south);
        var ymax = mercY(windy.north);
        var xFactor = windy.width / (windy.east - windy.west);
        var yFactor = windy.height / (ymax - ymin);
        var y = mercY(deg2rad(lat));
        var x = (deg2rad(lon) - windy.west) * xFactor;
        y = (ymax - y) * yFactor; // y points south
        return [x, y];
    };

    var interpolateField = function interpolateField(grid, bounds, extent, callback) {
        var projection = {};
        var mapArea = (extent.south - extent.north) * (extent.west - extent.east);
        var velocityScale = VELOCITY_SCALE * Math.pow(mapArea, 0.4);
        var columns = [];
        var x = bounds.x;

        function interpolateColumn(x) {
            var column = [];
            for (var y = bounds.y; y <= bounds.yMax; y += 2) {
                var coord = invert(x, y, extent);
                if (coord) {
                    var λ = coord[0],
                        φ = coord[1];
                    if (isFinite(λ)) {
                        var wind = grid.interpolate(λ, φ);
                        if (wind) {
                            wind = distort(projection, λ, φ, x, y, velocityScale, wind, extent);
                            column[y + 1] = column[y] = wind;
                        }
                    }
                }
            }
            columns[x + 1] = columns[x] = column;
        }
        (function batchInterpolate() {
            var start = Date.now();
            while (x < bounds.width) {
                interpolateColumn(x);
                x += 2;
                if (Date.now() - start > 1000) {
                    //MAX_TASK_TIME) {
                    setTimeout(batchInterpolate, 25);
                    return;
                }
            }
            createField(columns, bounds, callback);
        })();
    };

    var animationLoop;
    var animate = function animate(bounds, field) {
        function windIntensityColorScale(min, max) {
            colorScale.indexFor = function (m) {
                // map velocity speed to a style
                return Math.max(0, Math.min(colorScale.length - 1, Math.round((m - min) / (max - min) * (colorScale.length - 1))));
            };
            return colorScale;
        }

        var colorStyles = windIntensityColorScale(MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY);
        var buckets = colorStyles.map(function () {
            return [];
        });

        var particleCount = Math.round(bounds.width * bounds.height * PARTICLE_MULTIPLIER);
        particleCount = particleCount * AREA_OF;
        particleCount = Math.round(particleCount / Math.pow(Magnification, 2));

        // var fadeFillStyle = "rgba(0, 0, 0, 0.97)";

        var particles = [];
        for (var i = 0; i < particleCount; i++) {
            particles.push(field.randomize({
                age: Math.floor(Math.random() * MAX_PARTICLE_AGE) + 0
            }));
        }

        function evolve() {
            buckets.forEach(function (bucket) {
                bucket.length = 0;
            });
            particles.forEach(function (particle) {
                if (particle.age > MAX_PARTICLE_AGE) {
                    field.randomize(particle).age = 0;
                }
                var x = particle.x;
                var y = particle.y;
                var v = field(x, y); // vector at 当前的 position
                var m = v[2];
                if (m === null) {
                    particle.age = MAX_PARTICLE_AGE; // particle has escaped the grid, never to return...
                } else {
                    var xt = x + v[0];
                    var yt = y + v[1];
                    if (field(xt, yt)[2] !== null) {
                        // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket.
                        particle.xt = xt;
                        particle.yt = yt;
                        buckets[colorStyles.indexFor(m)].push(particle);
                    } else {
                        // Particle isn't visible, but it still moves through the field.
                        particle.x = xt;
                        particle.y = yt;
                    }
                }
                particle.age += 1;
            });
        }

        var g = params.canvas.getContext("2d");
        g.lineWidth = PARTICLE_LINE_WIDTH * Magnification;
        // g.fillStyle = fadeFillStyle;

        function draw() {
            // Fade existing particle trails.
            var prev = "lighter";
            g.globalCompositeOperation = "destination-in";
            g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
            g.globalCompositeOperation = prev;
            g.globalAlpha = GLOBALALPHA;

            buckets.forEach(function (bucket, i) { // Draw new particle trails.
                if (bucket.length > 0) {
                    g.beginPath();
                    g.strokeStyle = colorStyles[i];
                    if (['wave', 'globalWave',].includes(params.type)) {
                        g.lineWidth = (8 + i * 0.75) * Magnification;
                    }
                    bucket.forEach(function (particle) {
                        g.moveTo(particle.x, particle.y);
                        g.lineTo(particle.xt, particle.yt);
                        particle.x = particle.xt;
                        particle.y = particle.yt;
                    });
                    g.stroke();
                }
            });
        }

        var then = Date.now();
        (function frame() {
            animationLoop = requestAnimationFrame(frame);
            var now = Date.now();
            var delta = now - then;
            if (delta > (1000 / FRAME_RATE)) {
                then = now - delta % (1000 / FRAME_RATE);
                evolve();
                draw();
            }
        })();
    };

    var start = function start(bounds, width, height, extent) {

        var mapBounds = {
            south: deg2rad(extent[0][1]),
            north: deg2rad(extent[1][1]),
            east: deg2rad(extent[1][0]),
            west: deg2rad(extent[0][0]),
            width: width,
            height: height
        };

        stop();

        buildGrid(gridData, function (grid) { // build grid
            interpolateField(grid, buildBounds(bounds, width, height), mapBounds, function (bounds, field) { // interpolateField
                // animate the canvas with random points
                windy.field = field;
                animate(bounds, field);
            });
        });
    };

    var stop = function stop() {
        if (windy.field) windy.field.release();
        if (animationLoop) cancelAnimationFrame(animationLoop);
    };

    /**设置尺寸夸大比例*/
    var setMagnification = function setMagnification(value) {
        Magnification = value;
    };

    /**设置区域占比*/
    var setAreaOf = function setAreaOf(value) {
        AREA_OF = value;
    };

    /**重置粒子-密度 */
    var resetNum = function resetNum(value) {
        PARTICLE_MULTIPLIER = 1 / value;
    };

    /**重置粒子-宽度 */
    var resetWidth = function resetWidth(value) {
        PARTICLE_LINE_WIDTH = value;
    };

    /**重置粒子-颜色 */
    var resetColor = function resetColor(value) {
        colorScale = value;
    };

    /**重置粒子-速率 */
    var resetFrame = function resetFrame(value) {
        FRAME_RATE = value;
    };

    /**重置粒子-步长 */
    var resetStepM = function resetStepM(value) {
        VELOCITY_SCALE = value;
    };

    /**重置粒子-周期 */
    var resetParticleAge = function resetParticleAge(value) {
        MAX_PARTICLE_AGE = value;
    };

    /**重置全局透明度 */
    var resetGlobalAlpha = function resetGlobalAlpha(value) {
        GLOBALALPHA = value;
    };

    var clearCanvas = function clearCanvas() {
        stop();
        if (params.canvas) params.canvas.height = params.canvas.height * 1;
    };

    var windy = {
        params: params,
        start: start,
        stop: stop,
        createField: createField,
        interpolatePoint: interpolate,
        setData: setData,
        clearCanvas: clearCanvas,
        setAreaOf: setAreaOf,
        setMagnification: setMagnification,
        resetNum: resetNum,
        resetWidth: resetWidth,
        resetColor: resetColor,
        resetFrame: resetFrame,
        resetStepM: resetStepM,
        resetParticleAge: resetParticleAge,
        resetGlobalAlpha: resetGlobalAlpha,
    };

    return windy;
};

if (!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function (id) {
        clearTimeout(id);
    };
}

export default function (options) {
    return new L.ParticleLayer(options);
}